package de.yvert.jingle.tools;

import de.yvert.jingle.ToneMapper;
import de.yvert.jingle.hdr.HdrImage2D;
import de.yvert.jingle.ldr.LdrImage2D;

public class ExrToneMapper implements ToneMapper
{

private static final float EXPOSURE_ADD = 2.47393f;
private static final float GAMMA = 2.2f;
private static final float GAMMA_CORRECT = 1/GAMMA;
private static final float MIDDLE_GRAY_TARGET = 84.66f;

private float defog = 0;
private float exposure = 0;
private float kneeLow = 0;
private float kneeHigh = 5;

private float m, kl, f;

public ExrToneMapper()
{/*OK*/}

public void setDefog(float defog)
{ this.defog = defog; }

public void setExposure(float exposure)
{ this.exposure = exposure; }

public void setKneeLow(float kneeLow)
{ this.kneeLow = kneeLow; }

public void setKneeHigh(float kneeHigh)
{ this.kneeHigh = kneeHigh; }

private float knee(float x, float g)
{ return (float) Math.log((x * g + 1) / g); }

private float findKneeF(float x, float y)
{
	float f0 = 0;
	float f1 = 1;
	
	while (knee(x, f1) > y)
	{
		f0 = f1;
		f1 = f1 * 2;
	}
	
	for (int i = 0; i < 30; ++i)
	{
		float f2 = (f0 + f1) / 2;
		float y2 = knee(x, f2);
		if (y2 < y)
			f1 = f2;
		else
			f0 = f2;
	}
	return (f0 + f1) / 2;
}

private byte convert(float value)
{
	if (false)
	{ // FIXME: should be correct, but doesn't work
		// defog
		value = Math.max(0, value-defog);
		// exposure
		value *= m;
		// knee
		if (value > kl) value = kl + knee(value-kl, f);
		// gamma correction
		value = (float) Math.pow(value, GAMMA_CORRECT);
		// scaling
		value *= MIDDLE_GRAY_TARGET;
		// clamping
		return LdrImage2D.clamp(value);
	}
	
	// defog
	value = Math.max(0, value-defog);
	// exposure
	value *= m;
	// knee
//	if (value > kl) value = kl + knee(value-kl, f);
	// gamma correction
//	value = (float) Math.pow(value, GAMMA_CORRECT);
	// scaling
	value *= MIDDLE_GRAY_TARGET;
	// clamping
	return LdrImage2D.clamp(value);
}

public void map(HdrImage2D in, LdrImage2D out)
{
	float[] from = in.getData();
	byte[] to = out.getData();
	
	kl = (float) Math.pow(2, kneeLow);
	float kh = (float) Math.pow(2, kneeHigh);
	float kd = (float) Math.pow(2, 3.5);
	
	m = (float) Math.pow(2, exposure+EXPOSURE_ADD);
	f = findKneeF(kh-kl, kd-kl);
	
	for (int offset = 0; offset < from.length; offset += 4)
	{
		to[offset+0] = convert(from[offset+0]);
		to[offset+1] = convert(from[offset+1]);
		to[offset+2] = convert(from[offset+2]);
	}
}

}
